Skip to content

fix(status): exit gracefully when no changes exist#759

Open
fsilvaortiz wants to merge 3 commits intoFission-AI:mainfrom
fsilvaortiz:fix/714-graceful-status-no-changes
Open

fix(status): exit gracefully when no changes exist#759
fsilvaortiz wants to merge 3 commits intoFission-AI:mainfrom
fsilvaortiz:fix/714-graceful-status-no-changes

Conversation

@fsilvaortiz
Copy link

@fsilvaortiz fsilvaortiz commented Feb 25, 2026

Summary

Changes

File Change
src/commands/workflow/shared.ts Extract getAvailableChanges as public export
src/commands/workflow/status.ts Early check + graceful exit when no changes
test/commands/artifact-workflow.test.ts 2 new tests (text + JSON graceful exit)
.changeset/graceful-status-no-changes.md Patch changeset
openspec/changes/graceful-status-no-changes/ OpenSpec change artifacts (proposal, design, specs, tasks)

Test plan

  • openspec status with no changes → friendly message, exit 0
  • openspec status --json with no changes → valid JSON, exit 0
  • openspec status with changes but no --change → error listing changes (preserved)
  • openspec status --change non-existent → error "not found" (preserved)
  • Full onboard scenario: initstatusnew changestatus --change works end-to-end
  • All existing tests pass (18 pre-existing zsh-installer failures unrelated)

🤖 Generated with Claude Code using claude-opus-4-6.

Summary by CodeRabbit

  • Bug Fixes

    • Status now exits gracefully (code 0) when no active changes exist: shows a friendly hint in text mode and returns {"changes": [], "message":"No active changes."} in JSON mode instead of failing.
  • Tests

    • Added tests verifying graceful status behavior for both text and JSON outputs when no changes are present.

@fsilvaortiz fsilvaortiz requested a review from TabishB as a code owner February 25, 2026 18:06
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60c5849 and 9dda436.

📒 Files selected for processing (1)
  • src/commands/workflow/shared.ts

📝 Walkthrough

Walkthrough

Exports getAvailableChanges and updates statusCommand to call it before validation; when no changes exist, openspec status now exits 0 and emits a friendly text message or JSON {"changes":[],"message":"No active changes."}. Existing validation semantics for other flows are unchanged.

Changes

Cohort / File(s) Summary
Changelog & OpenSpec docs
\.changeset/graceful-status-no-changes.md, openspec/changes/graceful-status-no-changes/.openspec.yaml, openspec/changes/graceful-status-no-changes/*
Added changeset and OpenSpec metadata plus design/proposal/spec/tasks documenting the graceful status behavior when no changes exist.
Specs
openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md
New spec describing expected text and JSON outputs and scenarios for status when no changes exist.
Core Implementation
src/commands/workflow/shared.ts, src/commands/workflow/status.ts
Exported getAvailableChanges(projectRoot) (formerly internal) and updated statusCommand to call it before validateChangeExists; added early-return paths that stop the spinner and return exit code 0 with appropriate text/JSON when no changes are found.
Tests
test/commands/artifact-workflow.test.ts
Added tests asserting openspec status exits 0 and emits the friendly text or JSON {"changes":[],"message":"No active changes."} when no changes exist.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as Client (openspec)
  participant Status as statusCommand
  participant Shared as getAvailableChanges
  participant FS as FileSystem

  CLI->>Status: run "openspec status" [no --change]
  Status->>Shared: getAvailableChanges(projectRoot)
  Shared->>FS: read openspec/changes directory
  FS-->>Shared: list of change dirs

  alt no changes found
    Status->>Status: stop spinner
    Status-->>CLI: output text "No active changes..." or JSON {"changes":[],"message":"No active changes."}
    Status-->>CLI: exit 0
  else changes exist
    Status->>Status: proceed to validateChangeExists (existing behavior)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I sniffed the changes, none in sight,
So I twitched my nose and set things right.
JSON or text, no crash or fright,
Exit zero — hop on, delight! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: the status command now exits gracefully when no changes exist, directly addressing the core issue.
Linked Issues check ✅ Passed All coding requirements from issue #714 are met: status exits with code 0 gracefully, displays user-friendly messages, supports both text and JSON outputs, and preserves existing validation behavior for other commands.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #714: exporting getAvailableChanges, updating statusCommand with graceful handling, adding tests, and creating change artifacts for documentation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Feb 25, 2026

Greptile Summary

This PR fixes issue #714 by making openspec status exit gracefully (exit code 0) when no changes exist, instead of throwing a fatal error. The implementation extracts getAvailableChanges as a public function from shared.ts and adds early checking in statusCommand to handle the no-changes case before calling validateChangeExists. Both text and JSON output modes are supported.

Key changes:

  • Extracted getAvailableChanges as a public utility function in shared.ts (refactoring, no behavior change)
  • Added early check in status.ts to gracefully handle no-changes case with appropriate messages for text and JSON modes
  • Added comprehensive tests covering both output modes with exit code 0 verification
  • Other commands (apply, show, instructions) remain unaffected and continue using strict validation

The implementation is clean, well-tested, and maintains backward compatibility. No issues found.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The change is a focused bug fix with clear scope. The refactoring (extracting getAvailableChanges) doesn't alter existing behavior, just makes the function reusable. The new graceful handling in statusCommand is well-isolated and properly tested with both text and JSON output modes. All existing validation paths remain unchanged, ensuring other commands are unaffected. Test coverage is comprehensive.
  • No files require special attention

Important Files Changed

Filename Overview
src/commands/workflow/shared.ts Extracted getAvailableChanges as public export; refactored existing logic without changing behavior
src/commands/workflow/status.ts Added graceful handling for no-changes case; supports both text and JSON output modes with exit code 0
test/commands/artifact-workflow.test.ts Added two tests for graceful exit behavior (text and JSON modes); both verify exit code 0 and appropriate output

Last reviewed commit: eca0298

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/workflow/shared.ts (1)

89-109: ⚠️ Potential issue | 🟡 Minor

Orphaned JSDoc comment belongs on validateChangeExists, not above getAvailableChanges.

The block at lines 89-92 was the original JSDoc for validateChangeExists. Inserting getAvailableChanges (with its own JSDoc at lines 93-96) immediately after it leaves the old comment orphaned — TypeScript tooling will silently discard it, and validateChangeExists now has no JSDoc at all.

♻️ Proposed fix
-/**
- * Validates that a change exists and returns available changes if not.
- * Checks directory existence directly to support scaffolded changes (without proposal.md).
- */
-/**
+/**
  * Returns the list of available change directory names under openspec/changes/.
  * Excludes the archive directory and hidden directories.
  */
 export async function getAvailableChanges(projectRoot: string): Promise<string[]> {
   ...
 }

+/**
+ * Validates that a change exists and returns available changes if not.
+ * Checks directory existence directly to support scaffolded changes (without proposal.md).
+ */
 export async function validateChangeExists(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/workflow/shared.ts` around lines 89 - 109, Move the orphaned
JSDoc so it documents the correct function: remove the stray comment block
currently sitting above getAvailableChanges and place it immediately above the
validateChangeExists function declaration; ensure getAvailableChanges retains
its own JSDoc (the comment that currently documents it should remain directly
above getAvailableChanges) so both getAvailableChanges and validateChangeExists
have appropriate, non-duplicate JSDoc blocks.
🧹 Nitpick comments (1)
src/commands/workflow/status.ts (1)

44-57: Double getAvailableChanges call when --change is omitted and changes exist.

When options.change is undefined and available.length > 0, getAvailableChanges(projectRoot) is called at line 45, then called again inside validateChangeExists at line 57 (which always calls it when changeName is undefined). Two readdir round-trips for the same path in the same request.

♻️ Proposed fix — pass the already-resolved list to `validateChangeExists`

One approach: short-circuit entirely once you have available, skipping the second validateChangeExists call in the no---change branch.

-    if (!options.change) {
-      const available = await getAvailableChanges(projectRoot);
-      if (available.length === 0) {
-        spinner.stop();
-        if (options.json) {
-          console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
-          return;
-        }
-        console.log('No active changes. Create one with: openspec new change <name>');
-        return;
-      }
-    }
-
-    const changeName = await validateChangeExists(options.change, projectRoot);
+    let changeName: string;
+    if (!options.change) {
+      const available = await getAvailableChanges(projectRoot);
+      if (available.length === 0) {
+        spinner.stop();
+        if (options.json) {
+          console.log(JSON.stringify({ changes: [], message: 'No active changes.' }, null, 2));
+          return;
+        }
+        console.log('No active changes. Create one with: openspec new change <name>');
+        return;
+      }
+      // Changes exist but --change not provided — replicate the validateChangeExists error path
+      spinner.stop();
+      throw new Error(
+        `Missing required option --change. Available changes:\n  ${available.join('\n  ')}`
+      );
+    } else {
+      changeName = await validateChangeExists(options.change, projectRoot);
+    }

Alternatively, this is low-impact enough that a comment acknowledging the duplicate is sufficient if you prefer to keep validateChangeExists as the single source of truth for that error message.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/workflow/status.ts` around lines 44 - 57, When options.change is
undefined you call getAvailableChanges(projectRoot) and then
validateChangeExists(...) will call getAvailableChanges again; avoid the
duplicate readdir by extending validateChangeExists to accept an optional
precomputed list (e.g., available: string[] | undefined) and use that list if
provided instead of calling getAvailableChanges internally, then call
validateChangeExists(options.change, projectRoot, available) from status.ts when
you already have the available array; update the validateChangeExists signature
and its callers accordingly so the second disk lookup is skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/graceful-status-no-changes/design.md`:
- Line 37: The risk description in the design text is self-contradictory; update
the sentence referencing getAvailableChanges, validateChangeExists and
statusCommand so it correctly states that the extra filesystem read occurs when
changes do exist (i.e., getAvailableChanges returns results and statusCommand
proceeds to call validateChangeExists), and that when no changes exist
statusCommand returns early and only one read happens; reference
getAvailableChanges, validateChangeExists, and statusCommand in the corrected
sentence for clarity.

---

Outside diff comments:
In `@src/commands/workflow/shared.ts`:
- Around line 89-109: Move the orphaned JSDoc so it documents the correct
function: remove the stray comment block currently sitting above
getAvailableChanges and place it immediately above the validateChangeExists
function declaration; ensure getAvailableChanges retains its own JSDoc (the
comment that currently documents it should remain directly above
getAvailableChanges) so both getAvailableChanges and validateChangeExists have
appropriate, non-duplicate JSDoc blocks.

---

Nitpick comments:
In `@src/commands/workflow/status.ts`:
- Around line 44-57: When options.change is undefined you call
getAvailableChanges(projectRoot) and then validateChangeExists(...) will call
getAvailableChanges again; avoid the duplicate readdir by extending
validateChangeExists to accept an optional precomputed list (e.g., available:
string[] | undefined) and use that list if provided instead of calling
getAvailableChanges internally, then call validateChangeExists(options.change,
projectRoot, available) from status.ts when you already have the available
array; update the validateChangeExists signature and its callers accordingly so
the second disk lookup is skipped.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7d1860 and eca0298.

📒 Files selected for processing (9)
  • .changeset/graceful-status-no-changes.md
  • openspec/changes/graceful-status-no-changes/.openspec.yaml
  • openspec/changes/graceful-status-no-changes/design.md
  • openspec/changes/graceful-status-no-changes/proposal.md
  • openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md
  • openspec/changes/graceful-status-no-changes/tasks.md
  • src/commands/workflow/shared.ts
  • src/commands/workflow/status.ts
  • test/commands/artifact-workflow.test.ts

Extract `getAvailableChanges` as a public function from `validateChangeExists`
and use it in `statusCommand` to detect the no-changes case early. Returns a
friendly message (text and JSON modes) with exit code 0 instead of a fatal error.

Generated with Claude Code using claude-opus-4-6.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@fsilvaortiz fsilvaortiz force-pushed the fix/714-graceful-status-no-changes branch from eca0298 to 738d6a1 Compare February 25, 2026 18:15
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
openspec/changes/graceful-status-no-changes/design.md (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Self-contradictory risk/mitigation sentence — previous review comment not yet addressed.

The risk clause states the extra filesystem read occurs "when no changes exist", while the mitigation in the same sentence says "the extra read only happens when changes do exist." These two halves directly contradict each other and are still present in the current code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openspec/changes/graceful-status-no-changes/design.md` at line 37, The
sentence describing the risk/mitigation is self-contradictory: clarify whether
the extra filesystem read happens when no changes exist or only when changes do
exist by updating the sentence in design.md; reference the behavior of
getAvailableChanges, validateChangeExists and statusCommand to state precisely
that statusCommand returns early when no --change is provided and no changes
exist (avoiding validateChangeExists and the second read), so the extra read
only occurs when changes exist (because validateChangeExists will call
getAvailableChanges again); rewrite the line to remove the contradiction and
make the condition for the extra read explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openspec/changes/graceful-status-no-changes/proposal.md`:
- Around line 17-19: Update the "Modified Capabilities" note to mention that
while behavior is unchanged, the internal implementation of validateChangeExists
was refactored to delegate to the newly exported getAvailableChanges; explicitly
reference statusCommand (unchanged), validateChangeExists (refactored), and
getAvailableChanges so readers know the function was re-implemented not left
untouched.

In `@src/commands/workflow/shared.ts`:
- Around line 100-102: The catch in getAvailableChanges currently swallows all
errors and returns []; change it to only suppress ENOENT (directory not present)
and re-throw any other errors so permission or IO errors surface: inside
getAvailableChanges catch the error, if (err.code === 'ENOENT') return [];
otherwise throw err (or rethrow) so callers like statusCommand see real
filesystem errors.

---

Duplicate comments:
In `@openspec/changes/graceful-status-no-changes/design.md`:
- Line 37: The sentence describing the risk/mitigation is self-contradictory:
clarify whether the extra filesystem read happens when no changes exist or only
when changes do exist by updating the sentence in design.md; reference the
behavior of getAvailableChanges, validateChangeExists and statusCommand to state
precisely that statusCommand returns early when no --change is provided and no
changes exist (avoiding validateChangeExists and the second read), so the extra
read only occurs when changes exist (because validateChangeExists will call
getAvailableChanges again); rewrite the line to remove the contradiction and
make the condition for the extra read explicit.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eca0298 and 738d6a1.

📒 Files selected for processing (9)
  • .changeset/graceful-status-no-changes.md
  • openspec/changes/graceful-status-no-changes/.openspec.yaml
  • openspec/changes/graceful-status-no-changes/design.md
  • openspec/changes/graceful-status-no-changes/proposal.md
  • openspec/changes/graceful-status-no-changes/specs/graceful-status-empty/spec.md
  • openspec/changes/graceful-status-no-changes/tasks.md
  • src/commands/workflow/shared.ts
  • src/commands/workflow/status.ts
  • test/commands/artifact-workflow.test.ts
✅ Files skipped from review due to trivial changes (1)
  • openspec/changes/graceful-status-no-changes/.openspec.yaml
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/commands/workflow/status.ts
  • openspec/changes/graceful-status-no-changes/tasks.md
  • test/commands/artifact-workflow.test.ts
  • .changeset/graceful-status-no-changes.md

Address CodeRabbit review feedback:
- Fix contradictory risk description in design.md (double-read happens
  when changes exist, not when they don't)
- Clarify in proposal.md that validateChangeExists was internally
  refactored to delegate to getAvailableChanges

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
return entries
.filter((e) => e.isDirectory() && e.name !== 'archive' && !e.name.startsWith('.'))
.map((e) => e.name);
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice extraction into getAvailableChanges. One thing to tighten here: this catch treats every readdir error as "no changes". If there is a permission or filesystem issue, status can look successful and report no active changes. Could we return [] only for ENOENT, and rethrow other errors with a clear message?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — fixed in 9dda436. getAvailableChanges now only returns [] for ENOENT (directory doesn't exist) and rethrows everything else. This follows the same pattern used in src/telemetry/config.ts.

Verified:

  • ENOENT (no openspec/changes/ dir) → returns [], status exits 0 with "No active changes"
  • EACCES (permission denied) → error propagates, status exits 1 with the real error message

All 55 artifact-workflow tests pass.

Return [] only when the changes directory doesn't exist (ENOENT).
Rethrow other errors (EACCES, etc.) so real filesystem issues
surface instead of being silently masked as "no changes".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feedback: Onboards fails because openspec status returns : Error: No changes found. Create one with: openspec new change <name>

2 participants